﻿/*******************************************************************************
 * You may amend and distribute as you like, but don't remove this header!
 *
 * PblReader provides the ability to read PowerBuilder PBL files to extract source code.
 * See http://pblreader.codeplex.com/ for details.
 *
 * Copyright (C) 2012  Wagner DosAnjos
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU Lesser General Public License for more details.
 *
 * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
 * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
 *
 * All code and executables are provided "as is" with no warranty either express or implied. 
 * The author accepts no liability for any damage or loss of business that this product may cause. 
 *******************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using Pbl.Parser;

namespace Pbl.Reader
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// http://www.dwox.com/PBL_File_Format.txt
    /// +--------------------------------------------------------------+
    /// I PBL File Format                                  2003 - 2010 I
    /// +--------------------------------------------------------------+
    /// Dear PB Fans out there,
    /// 
    /// these are the results of the analysis I did, written down as
    /// a short ASCII text description (valid thru PB5-11.5).
    /// 
    /// With this knowledge you can write your own LibraryDirectory
    /// or Export Function for PowerBuilder PBL/PBD/DLL/EXE files.
    /// 
    /// Think about the possibility; including files via PBR assignment
    /// and extracting them during runtime. That is a nice gimmick.
    /// 
    /// Most of the terms used are the results and presumptions of my 
    /// analysis.
    /// 
    /// Thanks to: 
    ///   - Kevin Cai for Bytes 17-18 of the Node-Block
    ///   - Jeremy Lakeman for Bytes 19-20, 23-24 of the Node-Block
    /// 
    /// Regards
    /// 
    /// Arnd Schmidt                                          April 2011
    /// 
    /// 
    /// 
    /// arnd.schmidt@dwox.com
    /// 
    /// +--------------------------------------------------------------+
    /// I PBL File Format                                              I
    /// +--------------------------------------------------------------+
    /// 
    /// Rules and facts:
    /// 
    /// 1.) A PBL is always made out of blocks of 512, except the Node 
    /// Block (NOD*), that has a size of 6 blocks, meaning 3072 Bytes.
    /// 
    /// 2.) There is always one Header (HDR*) block, 
    /// followed by a free/used blocks bitmap (FRE*). 
    /// Then follows the first 'NOD*' block . 
    /// Theoretically this first 'NOD*' block might(!) point to a 
    /// parent node, but I have never seen that.
    /// 
    /// 3.) Object Data (also SCC Informations) are always 
    /// stored in single forward linked/chained of 'DAT*'-Blocks.
    /// 
    /// The information about the offset and the length is stored in 
    /// the Header (HDR*).
    /// 
    /// 4.) A PBD is a PBL.
    /// 
    /// 5.) DLL and EXE files have a 'TRL*' at the end of the file. 
    /// This is pointing to the one and only 'HDR*'-Block.
    /// Attention: 
    /// For signed DLLs (like PowerBuilder's signed DLLs in Version 11.5)
    /// you have to recalculate the offset to the 'TRL*' Block.
    /// 
    /// +--------------------------------------------------------------+
    /// I Library Header Block (512 Byte)                              I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'HDR*'                              I
    /// I   5 - 18  I String     I 'PowerBuilder' + 0x00 + 0x00        I
    /// I  19 - 22  I Char(4)    I PBL Format Version? (0400/0500/0600)I
    /// I  23 - 26  I Long       I Creation/Optimization Datetime      I
    /// I  29 - xx  I String     I Library Comment                     I
    /// I 285 - 288 I Long       I Offset of first SCC data block      I
    /// I 289 - 292 I Long       I Size (Net size of SCC data)         I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I Library Header Block - Unicode (1024 Byte)                   I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'HDR*'                              I
    /// I   5 - 32  I StringW    I 'PowerBuilder' + 0x00 + 0x00        I
    /// I  33 - 40  I CharW(4)   I PBL Format Version? (0400/0500/0600)I
    /// I  41 - 44  I Long       I Creation/Optimization Datetime      I
    /// I  47 - xx  I StringW    I Library Comment                     I
    /// I 559 - 562 I Long       I Offset of first SCC data block      I
    /// I 563 - 566 I Long       I Size (Net size of SCC data)         I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I  Bitmap Block (512 Byte)                                     I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I  1 - 4    I Char(4)    I 'FRE*'                              I
    /// I  5 - 8    I Long       I Offset of next block or 0           I
    /// I  9 - 512  I Bit(504)   I Bitmap, each Bit represents a block I
    /// +-----------+------------+-------------------------------------+
    /// (512 - 8) * 8 = 4032 Blocks are referenced
    /// 
    /// +--------------------------------------------------------------+
    /// I Node Block (3072 Byte)                                       I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'NOD*'                              I
    /// I   5 - 8   I Long       I Offset of next (left ) block or 0   I
    /// I   9 - 12  I Long       I Offset of parent block or 0         I
    /// I  13 - 16  I Long       I Offset of next (right) block or 0   I
    /// I  17 - 18  I Integer    I Space left in block, initial = 3040 I
    /// I  19 - 20  I Integer    I Position of alphabetically          I
    /// I           I            I first Objectname in this block      I
    /// I  21 - 22  I Integer    I Count of entries in that node       I
    /// I  23 - 24  I Integer    I Position of alphabetically          I
    /// I           I            I last Objectname in this block       I
    /// I  33 - xx  I Chunks     I 'ENT*'-Chunks                       I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I Entry Chunk (Variable Length)                                I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'ENT*'                              I
    /// I   5 - 8   I Char(4)    I PBL version? (0400/0500/0600)       I
    /// I   9 - 12  I Long       I Offset of first data block          I
    /// I  13 - 16  I Long       I Objectsize (Net size of data)       I
    /// I  17 - 20  I Long       I Unix datetime                       I
    /// I  21 - 22  I Integer    I Length of Comment                   I
    /// I  23 - 24  I Integer    I Length of Objectname                I
    /// I  25 - xx  I String     I Objectname                          I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I Entry Chunk - Unicode (Variable Length)                      I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'ENT*'                              I
    /// I   5 - 12  I CharW(4)   I PBL version? (0400/0500/0600)       I
    /// I  13 - 16  I Long       I Offset of first data block          I
    /// I  17 - 20  I Long       I Objectsize (Net size of data)       I
    /// I  21 - 24  I Long       I Unix datetime                       I
    /// I  25 - 26  I Integer    I Length of Comment                   I
    /// I  27 - 28  I Integer    I Length of Objectname                I
    /// I  29 - xx  I StringW    I Objectname                          I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I Data Block (512 Byte)                                        I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'DAT*'                              I
    /// I   5 - 8   I Long       I Offset of next data block or 0      I
    /// I   9 - 10  I Integer    I Length of data in block             I
    /// I  11 - XXX I Blob{}     I Data (maximum Length is 502         I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I Trailer Block (in DLL/EXE) always last block (512 Byte)      I
    /// +-----------+------------+-------------------------------------+
    /// I Pos.      I Type       I Information                         I
    /// +-----------+------------+-------------------------------------+
    /// I   1 - 4   I Char(4)    I 'TRL*'                              I
    /// I   5 - 8   I Long       I Offset of Library Header ('HDR*')   I
    /// +-----------+------------+-------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I SCC DATA                                                     I
    /// I     Structure of status information chunks                   I
    /// I     in DAT*-blocks (Variable Length)                         I
    /// +---------+----------------------------------------------------I
    /// I Type    I Information                                        I
    /// +---------+----------------------------------------------------I
    /// I String  I Libraryname (the opposite!)                        I
    /// I String  I Objectname                                         I
    /// I String  I Developername                                      I
    /// I Char(1) I Flag                                               I
    /// +---------+----------------------------------------------------I
    /// 
    /// +--------------------------------------------------------------+
    /// I PB6/7 Status Flags                                           I
    /// +------+------+------------------------------------------------+
    /// I Icon I Flag I Meaning                                        I
    /// +------+------+------------------------------------------------+
    /// I      I  r   I Object is registered                           I
    /// I      I  d   I Object is Checked Out (locked)                 I
    /// I      I  s   I Object (Working Copy) to be checked in         I
    /// I      I  u   I Unknown?! After an Error occurred.             I
    /// I      I      I (Checked out by user <Unknown>                 I
    /// I      I      I  Could be set to 'r' with an Hex-Editor.)      I
    /// +------+------+------------------------------------------------+
    /// 
    /// +--------------------------------------------------------------+
    /// I SCC DATA chunk                                               I
    /// I In newer PB Versions the DAT*-blocks content starts with the I
    /// I ansi-encoded String 'SCC*'.                                  I
    /// I Objectname and Version Informations are stored as            I
    /// I 0-Byte (Word) separated strings.                             I
    /// +----------+---------------------------------------------------I
    /// I Type     I Information                                       I
    /// +----------+------------+--------------------------------------+
    /// I  1 - 4   I Char(4)    I 'SCC*'                               I
    /// I  5 - xxx I Blob       I Objectname (string) followed by      I
    /// I          I            I Null-Byte 0x00 (Word in Unicode)     I
    /// I          I            I indicating the string end            I
    /// I          I            I Version (String) followed by         I
    /// I          I            I Null-Byte 0x00 (Word in Unicode)     I
    /// I          I            I indicating the string end            I
    /// I          I            I Next Objectname und Versioninfo      I
    /// I          I            I repeatedly until the end             I
    /// +----------+------------+--------------------------------------+
    /// 
    /// DateTimes are stored in Long format in Unix representation.
    /// Timezone is always GMT (+/- 0:00), so the datetime has to be
    /// converted to LocalDateTime via LocalTimeZone conversation.
    /// 
    /// In the compiled object data blocks, there are at least 2 more 
    /// datetimes, starting at byte 23 and the other one at 27!
    /// Looks like these are the modification and regeneration date...
    ///</remarks>
    public class PblReader : IDisposable
    {
        private const int HDR_BLOCK_SIZE = 512;
        private const int HDR_UNICODE_BLOCK_SIZE = 1024;
        private const int FRE_BLOCK_SIZE = 512;
        private const int NOD_BLOCK_SIZE = 3072;
        private const int DAT_BLOCK_SIZE = 512;
        private const int CONTENT_CAPACITY = 256 * 1024; // 256K

        private static DateTime UNIX_EPOCH = new DateTime(1970, 1, 1);

        private FileStream _pblStream;

        #region Properties
        public bool IsUnicode { get; private set; }
        public string FileName { get; private set; }
        public string Name { get; private set; }
        public string Stamp { get; private set; }
        public string Version { get; private set; }
        public DateTime CreatedDateTime { get; private set; }
        public string Comments { get; private set; }
        public PblEntryCollection Entries { get; private set; }
        #endregion

        public PblReader(string pblFileName)
        {
            this.Entries = new PblEntryCollection();
            this.FileName = pblFileName;
            this.Name = Path.GetFileName(this.FileName);

            _pblStream = File.OpenRead(pblFileName);

            ReadDirectory();
        }

        public void Close()
        {
            if (_pblStream != null)
            {
                _pblStream.Close();
                _pblStream = null;
            }
        }

        void IDisposable.Dispose()
        {
            Close();
        }

        public string GetContentAsString(PblEntry entry)
        {
            if (entry.IsBinary)
            {
                return null;
            }

            var data = new StringBuilder(CONTENT_CAPACITY);
            var block = new byte[DAT_BLOCK_SIZE];

            using (var fs = File.OpenRead(this.FileName))
            {
                for (long offset = entry.OffsetFirstDataBlock; offset != 0; )
                {
                    fs.Position = offset;

                    fs.Read(block, 0, block.Length);

                    var dat_type = Encoding.ASCII.GetString(block, 0x00, 4);
                    var dat_offset_next = BitConverter.ToInt32(block, 0x04);
                    var dat_length = BitConverter.ToInt16(block, 0x08);
                    var dat_data = this.IsUnicode
                                 ? Encoding.Unicode.GetString(block, 0x0A, dat_length)
                                 : Encoding.ASCII.GetString(block, 0x0A, dat_length);

                    data.Append(dat_data);

                    offset = dat_offset_next;
                }
            }

            return data.ToString(entry.CommentSize, data.Length - entry.CommentSize);
        }

        public byte[] GetContentAsByteArray(PblEntry entry)
        {
            var data = new List<byte>(CONTENT_CAPACITY);
            var block = new byte[DAT_BLOCK_SIZE];

            using (var fs = File.OpenRead(this.FileName))
            {
                for (long offset = entry.OffsetFirstDataBlock; offset != 0; )
                {
                    fs.Position = offset;

                    fs.Read(block, 0, block.Length);

                    var dat_type = Encoding.ASCII.GetString(block, 0x00, 4);
                    var dat_offset_next = BitConverter.ToInt32(block, 0x04);
                    var dat_length = BitConverter.ToInt16(block, 0x08);

                    data.AddRange(block.Skip(0x0A).Take(dat_length));

                    offset = dat_offset_next;
                }
            }

            return data.Skip(entry.CommentSize).ToArray();
        }

        public XmlDocument ParseDataWindow(PblEntry entry)
        {
            if (Path.GetExtension(entry.Name) != ".srd")
            {
                throw new NotSupportedException("Only data window entries (.srd) are supported.");
            }

            return SrdParser.Parse(GetContentAsString(entry));
        }

        public void Export(string targetFolder)
        {
            foreach (var entry in this.Entries)
            {
                var file = Path.Combine(targetFolder, entry.Name);

                using (var writer = File.Create(file))
                {
                    var content = GetContentAsByteArray(entry);

                    writer.Write(content, 0, content.Length);
                }
            }
        }

        private void ReadDirectory()
        {
            // Check if it's an Unicode PBL
            _pblStream.Position = 5;

            this.IsUnicode = (_pblStream.ReadByte() == 0);

            // Reposition file and allocate buffers
            _pblStream.Position = 0;

            var hdr = new byte[this.IsUnicode ? HDR_UNICODE_BLOCK_SIZE : HDR_BLOCK_SIZE];
            var fre = new byte[FRE_BLOCK_SIZE];
            var nod = new byte[NOD_BLOCK_SIZE];

            //  'HDR*' Block
            _pblStream.Read(hdr, 0, hdr.Length);

            string hdr_type;
            int hdr_unknown;
            string hdr_comments;

            if (this.IsUnicode)
            {
                hdr_type = Encoding.ASCII.GetString(hdr, 0x00, 4);
                this.Stamp = Encoding.Unicode.GetString(hdr, 0x04, 24);
                this.Version = Encoding.Unicode.GetString(hdr, 0x20, 8);
                this.CreatedDateTime = UNIX_EPOCH.AddSeconds(BitConverter.ToInt32(hdr, 0x28));
                hdr_unknown = BitConverter.ToInt16(hdr, 0x2C);
                hdr_comments = Encoding.Unicode.GetString(hdr, 0x2E, hdr.Length - 762);
            }
            else
            {
                hdr_type = Encoding.ASCII.GetString(hdr, 0x00, 4);
                this.Stamp = Encoding.ASCII.GetString(hdr, 0x04, 12);
                this.Version = Encoding.ASCII.GetString(hdr, 0x12, 4);
                this.CreatedDateTime = UNIX_EPOCH.AddSeconds(BitConverter.ToInt32(hdr, 0x16));
                hdr_unknown = BitConverter.ToInt16(hdr, 0x1C);
                hdr_comments = Encoding.ASCII.GetString(hdr, 0x1E, hdr.Length - 0x1E);
            }

            this.Comments = hdr_comments.Remove(hdr_comments.IndexOf('\0'));

            // 'FRE*' Block
            _pblStream.Read(fre, 0, fre.Length);

            var fre_type = Encoding.ASCII.GetString(fre, 0x00, 4);
            var fre_offset = BitConverter.ToInt32(fre, 0x04);

            // 'NOD*' Blocks (Directory)
            var nod_blocks_offset = new List<long>() { _pblStream.Position };

            for (var i = 0; i < nod_blocks_offset.Count(); i++)
            {
                var nod_offset = nod_blocks_offset[i];

                _pblStream.Position = nod_offset;
                _pblStream.Read(nod, 0, nod.Length);

                var nod_type = Encoding.ASCII.GetString(nod, 0x00, 4);

                var nod_offset_previous = BitConverter.ToInt32(nod, 0x04);
                var nod_offset_parent = BitConverter.ToInt32(nod, 0x08);
                var nod_offset_next = BitConverter.ToInt32(nod, 0x0C);

                if (nod_offset_next != 0)
                {
                    nod_blocks_offset.Add(nod_offset_next);         // add block to directory block list
                }

                if (nod_offset_previous != 0)
                {
                    nod_blocks_offset.Add(nod_offset_previous);     // add block to directory block list
                }

                var nod_available = BitConverter.ToInt16(nod, 0x10);
                var nod_first_obj = BitConverter.ToInt16(nod, 0x12);
                var nod_count_obj = BitConverter.ToInt16(nod, 0x14);
                var nod_last_obj = BitConverter.ToInt16(nod, 0x16);

                for (int offset = 0x20; offset < nod.Length - nod_available; )
                {
                    var entry = new PblEntry();

                    var ent_type = Encoding.ASCII.GetString(nod, offset, 4);
                    offset += 4;

                    if (this.IsUnicode)
                    {
                        entry.PblVersion = Encoding.Unicode.GetString(nod, offset, 8);
                        offset += 8;
                    }
                    else
                    {
                        entry.PblVersion = Encoding.ASCII.GetString(nod, offset, 4);
                        offset += 4;
                    }

                    entry.OffsetFirstDataBlock = BitConverter.ToInt32(nod, offset);
                    offset += 4;

                    entry.ObjectSize = BitConverter.ToInt32(nod, offset);
                    offset += 4;

                    entry.Timestamp = UNIX_EPOCH.AddSeconds(BitConverter.ToInt32(nod, offset));
                    offset += 4;

                    entry.CommentSize = BitConverter.ToInt16(nod, offset);
                    offset += 2;

                    var ent_name_len = BitConverter.ToInt16(nod, offset);
                    offset += 2;

                    entry.Name = this.IsUnicode
                                     ? Encoding.Unicode.GetString(nod, offset, ent_name_len - 2)
                                     : Encoding.ASCII.GetString(nod, offset, ent_name_len - 1);
                    offset += ent_name_len;

                    var extension = Path.GetExtension(entry.Name).ToLower();

                    this.Entries[entry.Name] = entry;
                }
            }

            // Entry comments
            foreach (var entry in this.Entries)
            {
                if (entry.CommentSize > 0)
                {
                    var block = new byte[DAT_BLOCK_SIZE];

                    _pblStream.Position = entry.OffsetFirstDataBlock;

                    _pblStream.Read(block, 0, block.Length);

                    entry.Comment = this.IsUnicode
                                  ? Encoding.Unicode.GetString(block, 0x0A, entry.CommentSize * sizeof(char))
                                  : Encoding.ASCII.GetString(block, 0x0A, entry.CommentSize);
                }
            }
        }
    }
}
